using System;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using System.Xml.Xsl;
using AndyPike.Unfuddle.Contracts;
using AndyPike.Unfuddle.Entities;
using AndyPike.Unfuddle.Exceptions;
using AndyPike.Unfuddle.Factories;
using AndyPike.Unfuddle.Xslt;

namespace AndyPike.Unfuddle.Services
{
    /// <summary>
    /// An Unfuddle service that queries and expects xml from the server
    /// </summary>
    public class XmlUnfuddleService : IUnfuddleService
    {
        private readonly bool ssl;
        private readonly string subdomain;
        private readonly string username;
        private readonly string password;

        /// <summary>
        /// Allows the caller to change the default HttpWebRequestFactory if required
        /// </summary>
        public IHttpWebRequestFactory HttpWebRequestFactory { get; set; }
        
        /// <summary>
        /// Instantiates the XmlUnfuddleService service
        /// </summary>
        /// <param name="ssl">Allows the requests to be invoked over ssl if the account supports it</param>
        /// <param name="subdomain">The Unfuddle subdomain to use when issuing requests, for example: http://subdomain.unfuddle.com</param>
        /// <param name="username">The username of the Unfuddle account to use when issuing requests</param>
        /// <param name="password">The password of the Unfuddle account to use when issuing requests</param>
        public XmlUnfuddleService(bool ssl, string subdomain, string username, string password)
        {
            GuardAgainst.ArgumentNullOrEmpty(subdomain, "subdomain");
            GuardAgainst.ArgumentNullOrEmpty(username, "username");
            GuardAgainst.ArgumentNullOrEmpty(password, "password");

            this.ssl = ssl;
            this.subdomain = subdomain;
            this.username = username;
            this.password = password;

            //Default to this standard factory, but it can be replaced
            //via property (enables mocking during unit testing)
            HttpWebRequestFactory = new HttpWebRequestFactory();    
        }

        /// <summary>
        /// Creates an Unfuddle resource url for a particular action
        /// </summary>
        /// <param name="action">The action part of the url, for example: projects/{id}</param>
        /// <param name="args">Format args used to replace {n} in the action param</param>
        /// <returns>Returns the fully constructed resource url</returns>
        public string BuildUrl(string action, params object[] args)
        {
            GuardAgainst.ArgumentNullOrEmpty(action, "action");

            if (args != null && args.Length > 0)
                action = string.Format(action, args);

            string protocol = ssl ? "https" : "http";
            return string.Format("{0}://{1}.unfuddle.com/api/v1/{2}.xml", protocol, subdomain, action);
        }

        /// <summary>
        /// Makes the http request for the given url and deserializes the response. Also checks the http 
        /// status code and throws exceptions as required. Defaults to GET http verb and doesn't include a request body.
        /// </summary>
        /// <typeparam name="T">The entity type that should be deserialized</typeparam>
        /// <param name="url">The Unfuddle url to request</param>
        /// <returns>Returns the deserialized object from the response</returns>
        public T InvokeRequest<T>(string url) where T : class, IEntity
        {
            return InvokeRequest<T>(url, HttpMethod.GET, null);
        }

        /// <summary>
        /// Makes the http request for the given url and deserializes the response (if GET). Also checks the http 
        /// status code and throws exceptions as required. Can serialize an entity to be sent as the request body.
        /// </summary>
        /// <typeparam name="T">The entity type that should be deserialized</typeparam>
        /// <param name="url">The Unfuddle url to request</param>
        /// <param name="httpMethod">The http verb tp use when issuing the request</param>
        /// <param name="entityToSend">The entity to serialize and send as part of the request body</param>
        /// <returns>Returns the deserialized object from the response</returns>
        public T InvokeRequest<T>(string url, HttpMethod httpMethod, T entityToSend) where T : class, IEntity
        {
            GuardAgainst.ArgumentNullOrEmpty(url, "url");

            T result = null;
            RequestException exception = null;
            HttpWebResponse response = null;
            HttpWebRequest request = HttpWebRequestFactory.Create(url, username, password, httpMethod);
            
            //If there is an entity to send up to the server, serialize it as the request body
            if (entityToSend != null)
            {
                SerializeEntity(entityToSend, request);
            }

            try
            {
                //Get the server response and deserialize it if we are part of a GET operation.
                //If the response returns a status code other than 200 (OK) or it throws an exception,
                //process the response for error information and throw an exception as required.
                response = (HttpWebResponse)request.GetResponse();
                if (response.StatusCode == HttpStatusCode.OK && httpMethod == HttpMethod.GET)
                {
                    result = DeserializeResponse<T>(response);
                }
                else if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
                {
                    exception = CreateRequestException(url, response);
                }
            }
            catch (WebException ex)
            {
                exception = CreateRequestException(url, (HttpWebResponse)ex.Response);
            }
            finally
            {
                if(response != null)
                    response.Close();
            }

            if (exception != null)
                throw exception;

            return result;
        }
        
        /// <summary>
        /// Processes a response that returned a status code other than 200 OK and creates the
        /// correct exception ready for throwing once the caller has closed up the response stream
        /// </summary>
        /// <param name="url">The url that was reqested</param>
        /// <param name="response">The invalid response from the server</param>
        private RequestException CreateRequestException(string url, HttpWebResponse response)
        {
            ErrorsCollection errors;
            try
            {
                errors = DeserializeResponse<ErrorsCollection>(response);
            }
            catch (Exception)
            {
                //The response may not contain error information, so handle this gracefully by
                //setting the errors to a new empty collection to make things easier for callers.
                errors = new ErrorsCollection();
            }

            switch(response.StatusCode)
            {
                case HttpStatusCode.BadRequest:
                    return new BadRequestException(url, errors);
                case HttpStatusCode.Unauthorized:
                    return new UnauthorizedRequestException(url, errors);
                case HttpStatusCode.NotFound:
                    return new NotFoundRequestException(url, errors);
                case HttpStatusCode.MethodNotAllowed:
                    return new MethodNotAllowedRequestException(url, errors);
                case HttpStatusCode.NotAcceptable:
                    return new NotAcceptableRequestException(url, errors);
                case HttpStatusCode.UnsupportedMediaType:
                    return new UnsupportedMediaTypeRequestException(url, errors);
                case HttpStatusCode.InternalServerError:
                    return new InternalServerErrorRequestException(url, errors);
                case HttpStatusCode.Forbidden:
                    return new ForbiddenRequestException(url, errors);
                default:
                    return new UnexpectedRequestException(url, errors, response.StatusCode);
            }
        }

        /// <summary>
        /// Executes an Unfuddle find command on this service
        /// </summary>
        /// <typeparam name="T">The type to return from the execution of this command</typeparam>
        /// <param name="command">The Unfuddle command to execute</param>
        /// <returns>Returns the result of the command (type T)</returns>
        public T Execute<T>(IUnfuddleFindCommand<T> command)
        {
            GuardAgainst.ArgumentNull(command, "command");

            return command.Execute(this);
        }

        /// <summary>
        /// Executes an Unfuddle change command on this service
        /// </summary>
        /// <param name="command">The Unfuddle command to execute</param>
        public void Execute(IUnfuddleChangeCommand command)
        {
            GuardAgainst.ArgumentNull(command, "command");

            command.Execute(this);
        }

        /// <summary>
        /// Deserializes the servers xml response and returns the objects
        /// </summary>
        /// <typeparam name="T">The entity type to deserialize to</typeparam>
        /// <param name="response">The server response to deserialize</param>
        /// <returns>Returns the deserialized entity</returns>
        private T DeserializeResponse<T>(WebResponse response) where T : IEntity
        {
            using (Stream stream = response.GetResponseStream())
            {
                //Remove any null elements so that the serializer uses the mapped property default 
                //instead of throwing an exception. The removed elements should be nullable.
                var xml = new XmlDocument();
                xml.Load(stream);

                XmlNodeList nullNodes = xml.DocumentElement.SelectNodes("//*[@nil='true']");
                foreach (XmlNode nullNode in nullNodes)
                {
                    Console.WriteLine("Removing the element '{0}' from '{1}' while deserializing becasue it is marked as null. The mapped entity property should be nullable.", nullNode.Name, xml.DocumentElement.Name);
                    nullNode.ParentNode.RemoveChild(nullNode);
                }

                using (var transformedXml = new MemoryStream())
                {
                    xml.Save(transformedXml);
                    transformedXml.Position = 0;

                    //Now deserialize the transformed xml response into an object graph of type T
                    var serializer = new XmlSerializer(typeof (T));
                    return (T) serializer.Deserialize(transformedXml);
                }
            }
        }

        /// <summary>
        /// Serialize an entity to a request
        /// </summary>
        /// <typeparam name="T">The entity type to serialize</typeparam>
        /// <param name="entityToSend">The entity to serialize</param>
        /// <param name="request">The WebRequest to serialize the entity to</param>
        private void SerializeEntity<T>(T entityToSend, WebRequest request) where T : IEntity
        {
            using (var stream = new MemoryStream())
            {
                //Serialize the entity to xml in memory
                var serializer = new XmlSerializer(typeof (T));
                serializer.Serialize(stream, entityToSend);
                stream.Position = 0;

                //Use xslt to strip the namespaces that .NET adds that Unfuddle cannot handle
                var xml = new XPathDocument(stream);
                var transformer = new XslCompiledTransform();
                transformer.Load(typeof(RemoveNamespaces));

                //Write the transformed xml to the request stream
                using (var writer = new StreamWriter(request.GetRequestStream(), Encoding.UTF8))
                {
                    transformer.Transform(xml, null, writer);
                }
            }
        }
    }
}